package ms.maplestory.boot.connection.netty;

import ms.maplestory.boot.client.Client;
import ms.maplestory.boot.client.Account;
import ms.maplestory.boot.client.character.Char;
import ms.maplestory.boot.handlers.Handler;
import ms.maplestory.boot.connection.InPacket;
import ms.maplestory.boot.connection.opcode.InHeader;
import ms.maplestory.boot.util.container.Tuple;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

import static ms.maplestory.boot.connection.netty.NettyClient.CLIENT_KEY;

@Slf4j(topic = "[Netty ChannelHandler]")
@Component
public class ChannelHandler extends SimpleChannelInboundHandler<InPacket> {

    private static final Map<InHeader, Tuple<Method, Object>> handlers = new HashMap<>();
    @Resource
    private ApplicationContext applicationContext;

    @EventListener(ApplicationReadyEvent.class)
    public void initHandlers() {
        long start = System.currentTimeMillis();
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(Handler.class);
        beans.forEach((name, value) -> {
            Stream.of(value.getClass().getDeclaredMethods())
                  .forEach(method -> {
                      Handler handler = method.getAnnotation(Handler.class);
                      if (handler != null) {
                          InHeader header = handler.op();
                          if (header != InHeader.CP_None) {
                              if (handlers.containsKey(header)) {
                                  throw new IllegalArgumentException(String.format("Multiple handlers found for header %s! " +
                                          "Had method %s, but also found %s.", header, handlers.get(header).getLeft(), method.getName()));
                              }
                              handlers.put(header, new Tuple<>(method, value));
                          }
                          InHeader[] headers = handler.ops();
                          for (InHeader h : headers) {
                              handlers.put(h, new Tuple<>(method, value));
                          }
                      }
                  });
        });

        log.info("Initialized {} handlers in {} ms.", handlers.size(), (System.currentTimeMillis() - start));
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        log.debug("[ChannelHandler] | Channel inactive.");
        Client c = (Client) ctx.channel().attr(CLIENT_KEY).get();
        if (Objects.isNull(c)) {
            return;
        }
        Account account = c.getAccount();
        Char chr = c.getChr();
        if (chr != null) {
            if (!chr.isChangingChannel()) {
                chr.getCharService().logout(chr);
            } else {
                chr.setChangingChannel(false);
            }
        } else if (account != null) {
            account.getAccountService().unstuck(account);
        } else {
            log.warn("[ChannelHandler] | Was not able to save character, data inconsistency may have occurred.");
        }
        NettyClient o = ctx.channel().attr(CLIENT_KEY).get();
        if (o != null) {
            o.close();
        }
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, InPacket inPacket) {
        Client c = (Client) ctx.channel().attr(CLIENT_KEY).get();
        Char chr = c.getChr();
        short op = inPacket.decodeShort();
        InHeader inHeader = InHeader.getInHeaderByOp(op);
        if (inHeader == null) {
            handleUnknown(inPacket, op);
            return;
        }
        if (!InHeader.isSpamHeader(InHeader.getInHeaderByOp(op))) { // 封包输出
            log.debug(String.format("%s, %d/0x%s\t| %s", InHeader.getInHeaderByOp(op), op, Integer.toHexString(op).toUpperCase(), inPacket));
        }
        try {
            Method method = handlers.get(inHeader).getLeft();
            if (method == null) {
                handleUnknown(inPacket, op);
            } else {
                try {
                    if (method.getParameterTypes().length <= 4) {
                        method.invoke(handlers.get(inHeader).getRight(), Stream.of(method.getParameterTypes()).map(it -> {
                            if (it == Client.class) {
                                return c;
                            } else if (it == Char.class) {
                                return chr;
                            } else if (it == InHeader.class) {
                                return inHeader;
                            } else if (it == InPacket.class) {
                                return inPacket;
                            }
                            return null;
                        }).toArray());
                    } else {
                        log.error("handler arguments number error " + method.getName() + ", handler = " + inHeader);
                    }
                } catch (IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            inPacket.release();
        }
    }

    private void handleUnknown(InPacket inPacket, short opCode) {
        if (!InHeader.isSpamHeader(InHeader.getInHeaderByOp(opCode))) {
            log.warn(String.format("Unhandled opcode %s/0x%s, packet %s", opCode, Integer.toHexString(opCode).toUpperCase(), inPacket));
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        if (cause instanceof IOException) {
            log.info("Client forcibly closed the game.");
        } else {
            cause.printStackTrace();
        }
    }
}
